#region
/* Adoor.NET - Object/Relational mapping framework leveraging ADO.NET`s powerful disconnected mode.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */ 
#endregion

using System;

namespace Adoor.Expression
{
	public enum ScannerToken 
	{
		And, As, Asc, Asterisk, By, BitWiseAnd, BitWiseNot, BitWiseOr,
		Comma, Colon, Date, Desc, Distinct, Double, End, Eq, Exclamation, Exists,
		False, From, Ge, Group, Gt, Having, In, Inner, Integer, Is, Join, 
		LeftBrace, Le, Left, LeftParen, Like, Lt, MinusSign,
		Name, Ne, Not, Null, On, Or, Order, Outer, Params, Period, PlaceHolder, PlusSign, QuestionMark, 
		RightBrace, RightParen, Select, Single, Solidus, String, Top, True, 
		Unexpected, Where, Modulo, Union, All, Update, Insert, Delete, Into, Set, Values
	}

	public class Scanner
	{
		public Scanner(string text)
		{
			this.text = new char[text.Length + 1];
			text.CopyTo(0, this.text, 0, text.Length);
			this.text[text.Length] = '\0';	// add a sentinel to avoid many (this.pos < this.test.Length) conditions
			this.pos = 0;
		}

		public string GetString()
		{
			return GetString(this.start, this.pos - this.start);
		}

		public string GetString(int start, int count)
		{
			if(Token == ScannerToken.Name && this.text[start] == '[')
				return new String(this.text, start + 1, count - 2);
			
			return new String(this.text, start, count);
		}

		public void Scan()
		{
			ScanWhite();
			this.start = this.pos;
			switch (this.text[this.pos])
			{
				case '\0':
					if (this.pos != this.text.Length - 1) 
						this.token = ScannerToken.Unexpected;
					else
						this.token = ScannerToken.End;
					++this.pos;
					break;
				case '&':
					++this.pos;
					this.token = ScannerToken.BitWiseAnd;
					break;
				case '|':
					++this.pos;
					this.token = ScannerToken.BitWiseOr;
					break;
				case '~':
					++this.pos;
					this.token = ScannerToken.BitWiseNot;
					break;
				case '(':
					++this.pos;
					this.token = ScannerToken.LeftParen;
					break;
				case ')':
					++this.pos;
					this.token = ScannerToken.RightParen;
					break;
				case '{':
					++this.pos;
					this.token = ScannerToken.LeftBrace;
					break;
				case '}':
					++this.pos;
					this.token = ScannerToken.RightBrace;
					break;
				case '.':
					++this.pos;
					this.token = ScannerToken.Period;
					break;
				case ',':
					++this.pos;
					this.token = ScannerToken.Comma;
					break;
				case '[':
					++this.pos;
					ScanEscapedName(']');
					break;
				case '+':
					++this.pos;
					token = ScannerToken.PlusSign;
					break;
				case '-':
					++this.pos;
					token = ScannerToken.MinusSign;
					break;
				case '*':
					++this.pos;
					token = ScannerToken.Asterisk;
					break;
				case '/':
					++this.pos;
					token = ScannerToken.Solidus;
					break;
				case '=':
					this.token = ScannerToken.Eq;
					++this.pos;
					break;
				case '<':
					++this.pos;
					if (this.text[this.pos] == '>') 
					{
						++this.pos;
						this.token = ScannerToken.Ne;
					}
					else if (this.text[this.pos] == '=')
					{
						++this.pos;
						this.token = ScannerToken.Le;
					}
					else
						this.token = ScannerToken.Lt;
					break;
				case '>':
					++this.pos;
					if (this.text[this.pos] == '=')
					{
						++this.pos;
						this.token = ScannerToken.Ge;
					}
					else
						this.token = ScannerToken.Gt;
					break;
				case '#':
					ScanDate();
					break;
				case '\'':
					ScanString('\'');
					break;
				case '"':
					ScanString('"');
					break;
				case ':':
					++this.pos;
					if(this.text[this.pos] == '?')
					{
						++this.pos;
						this.token = ScannerToken.PlaceHolder;
					}
					else
					{
						this.token = ScannerToken.Colon;						
					}
					break;	
				case '?':
					++this.pos;
					this.token = ScannerToken.QuestionMark;
					break;
				case '!':
					++this.pos;
					this.token = ScannerToken.Exclamation;
					break;
				case '%':
					++this.pos;
					this.token = ScannerToken.Modulo;
					break;
				default:
					if (IsDigit(this.text[this.pos])) 
						ScanNumeric();
					else if (IsAlphaNumeric(this.text[this.pos])) 
					{
						ScanName();
						string name = new String(this.text, this.start, this.pos - this.start);
						
						
						switch (name.ToUpper())
						{
							case "AND":
								token = ScannerToken.And;
								break;
							case "AS":
								token = ScannerToken.As;
								break;
							case "ASC":
								token = ScannerToken.Asc;
								break;
							case "BY":
								token = ScannerToken.By;
								break;
							case "DESC":
								token = ScannerToken.Desc;
								break;
							case "DISTINCT":
								if(NextToken() == ScannerToken.LeftParen)
								{
									this.token = ScannerToken.Name;
									break;
								}
								token = ScannerToken.Distinct;
								break;
							case "EXISTS":
								token = ScannerToken.Exists;
								break;
							case "FALSE":
								token = ScannerToken.False;
								break;
							case "FROM":
								token = ScannerToken.From;
								break;
							case "GROUP":
								token = ScannerToken.Group;
								break;
							case "HAVING":
								token = ScannerToken.Having;
								break;
							case "IN":
								token = ScannerToken.In;
								break;
							case "INNER":
								token = ScannerToken.Inner;
								break;
							case "IS":
								token = ScannerToken.Is;
								break;
							case "JOIN":
								token = ScannerToken.Join;
								break;
							case "LEFT":
								token = ScannerToken.Left;
								break;
							case "LIKE":
								token = ScannerToken.Like;
								break;
							case "NOT":
								token = ScannerToken.Not;
								break;
							case "NULL":
								token = ScannerToken.Null;
								break;
							case "ON":
								token = ScannerToken.On;
								break;
							case "OR":
								token = ScannerToken.Or;
								break;
							case "ORDER":
								token = ScannerToken.Order;
								break;
							case "OUTER":
								token = ScannerToken.Outer;
								break;
							case "SELECT":
								token = ScannerToken.Select;
								break;
							case "TOP":
								token = ScannerToken.Top;
								break;
							case "TRUE":
								token = ScannerToken.True;
								break;
							case "WHERE":
								token = ScannerToken.Where;
								break;
							case "UNION":
								token = ScannerToken.Union;
								break;
							case "ALL":
								token = ScannerToken.All;
								break;
							case "INSERT":
								token = ScannerToken.Insert;
								break;
							case "UPDATE":
								token = ScannerToken.Update;
								break;
							case "DELETE":
								token = ScannerToken.Delete;
								break;
							case "INTO":
								token = ScannerToken.Into;
								break;
							case "SET":
								token = ScannerToken.Set;
								break;
							case "VALUES":
								token = ScannerToken.Values;
								break;
							default:
								this.token = ScannerToken.Name;
								break;						
						}							
					}
					else 
					{
						++ this.pos;
						this.token = ScannerToken.Unexpected;
					}
					break;
			}
		}

		private void ScanDate()
		{
			do
			{
				++this.pos;
				if (this.text[this.pos] == '\0')
					throw new Exception("invalid date");
			}
			while (this.text[this.pos] != '#');

			if (this.text[this.pos] != '#')
				throw new Exception("invalid date");
			this.token = ScannerToken.Date;
			++this.pos;
		}

		private void ScanName()
		{
			while (IsAlphaNumeric(this.text[this.pos]))
				++this.pos;
			this.token = ScannerToken.Name;
		} 

		private void ScanEscapedName(char escapeChar)
		{
			ScanString(escapeChar);
			this.token = ScannerToken.Name;
		} 

		private void ScanNumeric()
		{
			bool hasSeparator = false;
			bool hasExponent = false;
			while (IsDigit(this.text[this.pos]))
				++this.pos;
			if (this.text[this.pos] == '.')
			{
				hasSeparator = true;
				++this.pos;
				if (!IsDigit(this.text[this.pos]))
					throw new Exception("digit expected");
				while (IsDigit(this.text[this.pos]))
					++this.pos; 
			}
			if (this.text[this.pos] == 'e' || this.text[this.pos] == 'E')
			{
				hasExponent = true;
				++this.pos;
				if ((this.text[this.pos] == '-') || (this.text[this.pos] == '+'))
					++this.pos;
				if (!IsDigit(this.text[this.pos]))
					throw new Exception("digit expected");
				while (IsDigit(this.text[this.pos]))
					++this.pos;
			}
 
			if (hasExponent)
				this.token = ScannerToken.Double;
			else if (hasSeparator)
				this.token = ScannerToken.Single;
			else
				this.token = ScannerToken.Integer; 
		}

		private void ScanString(char escape)
		{
			++this.pos;
			while (this.text[this.pos] != escape)
			{
				if (this.text[this.pos] == '\0')
					throw new Exception("invalid string"); 
				++this.pos;
			}
			++this.pos;
 
			this.token = ScannerToken.String; 
		} 

		private void ScanWhite()
		{
			while (IsWhiteSpace(this.text[this.pos]))
				++this.pos;
		}

		protected static bool IsWhiteSpace(char ch)
		{
			return ch <= ' ' && ch != 0;
		} 

		protected static bool IsAlphaNumeric(char ch)
		{
			return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '@';
		}

		protected static bool IsDigit(char ch)
		{
			return ch >= '0' && ch <= '9';
		}

		public ScannerToken Token
		{
			get { return this.token; }
			set { this.token = value; }
		}

		public int Pos
		{
			get { return this.pos; }
			set { this.pos = value; }
		}

		public int Start
		{
			get { return this.start; }
		}

		private char[] text;
		public char[] Text
		{
			get{ return text; }
		}

		private int start;
		private int pos;
		private ScannerToken token;

		private object tag;
		public object Tag
		{
			get{ return tag;}
			set{ tag = value; }
		}

		public ScannerToken NextToken()
		{
			if(Token == ScannerToken.End)
				return ScannerToken.End;

			int oldPos = this.pos;
			int oldStart = this.start;
			ScannerToken currentToken = Token;
			Scan();
			ScannerToken nextToken = Token;
			this.pos = oldPos;
			this.start = oldStart;
			Token = currentToken;
			return nextToken;
		}
	}
}